import { useRef, useCallback, useState } from "react";

type State = {
  inView: boolean;
  entry?: IntersectionObserverEntry;
};

type InViewHookResponse = {
  ref: (node?: Element | null) => void;
  inView: boolean;
  entry?: IntersectionObserverEntry;
};

type ObserverInstanceCallback = (
  inView: boolean,
  entry: IntersectionObserverEntry
) => void;

let instance: {
  observer: IntersectionObserver;
  elements: Map<Element, ObserverInstanceCallback>;
} | null = null;

const options: IntersectionObserverInit = {
  root: document.querySelector("#scrollArea"),
  rootMargin: "0px",
  threshold: 0.2,
};
function createObserver() {
  if (!instance) {
    const elements = new Map<Element, ObserverInstanceCallback>();
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        const inView = entry.isIntersecting;
        elements.get(entry.target)?.(inView, entry);
      });
    }, options);
    instance = {
      observer,
      elements,
    };
  }

  return instance;
}

function myObserve(element: Element, callback: ObserverInstanceCallback) {
  if (!element) return () => {};

  const { observer, elements } = createObserver();
  if (!elements.has(element)) {
    elements.set(element, callback);
  }
  observer.observe(element);

  return function unobserve() {
    elements.delete(element);
    observer.unobserve(element);

    if (elements.size === 0) {
      observer.disconnect();
    }
  };
}

export default function useInView(): InViewHookResponse {
  const unobserve = useRef<() => void>();
  const [state, setState] = useState<State>({
    inView: false,
    entry: undefined
  });
  const setRef = useCallback((node) => {
    if (unobserve.current !== undefined) {
      unobserve.current();
      unobserve.current = undefined;
    }

    if (node) {
      unobserve.current = myObserve(node, (inView, entry) => {
        setState({ inView, entry });
      });
    }
  }, []);

  const result: InViewHookResponse = {
    ref: setRef,
    inView: state.inView,
    entry: state.entry,
  };

  return result;
}
